[SEC] restrict CORS to authorized extension IDs#581
[SEC] restrict CORS to authorized extension IDs#581RaoufGhrissi wants to merge 1 commit intoActivityWatch:masterfrom
Conversation
325720e to
587bb8c
Compare
Greptile SummaryThis PR hardens CORS security by replacing the blanket Several issues raised in earlier review rounds have been resolved in this revision: Confidence Score: 5/5Safe to merge — all P0/P1 issues from the previous review round are resolved; remaining findings are P2 style and improvement suggestions. The significant defects identified in earlier rounds (wrong default for cors_allow_all_mozilla_extension, |= write-once boolean bug, state.config not updated after save, fragile trim_matches JSON parsing, testing-mode regex in wrong list) are all fixed. The datastore worker commit-flag bug is also correctly patched. Remaining findings are a validation inconsistency, missing test coverage, and a minor per-request filesystem read — none block merging. aw-server/src/endpoints/cors_config.rs (exact-origin validation) and aw-server/tests/api.rs (no coverage for new endpoints) Important Files Changed
Sequence DiagramsequenceDiagram
participant Client as Web UI / Client
participant Server as aw-server
participant Fairing as rocket_cors Fairing
participant Config as state.config (Mutex)
participant DB as Datastore (key-value)
participant TOML as config.toml
Note over Server: Startup
Server->>TOML: read config, identify missing CORS fields
Server->>DB: load missing fields from cors.* keys
Server->>Config: initialise AWConfig
Server->>Fairing: build_cors(config) — snapshot of origins
Note over Client,Fairing: Runtime — read current settings
Client->>Server: GET /api/0/cors-config
Server->>TOML: get_config_path() — re-read file to find in_file fields
Server->>Config: lock and read values
Server-->>Client: CorsConfig { ..., needs_restart: true }
Note over Client,Fairing: Runtime — save new settings
Client->>Server: POST /api/0/cors-config
Server->>DB: set_key_value(cors.*, ...) for non-file fields
Server->>Config: lock and update in-memory values
Server-->>Client: 200 OK
Note over Fairing: Fairing NOT updated — restart required
Reviews (8): Last reviewed commit: "[SEC] restrict CORS to authorized extens..." | Re-trigger Greptile |
aw-server/src/endpoints/cors.rs
Outdated
| let parse_cors_list = |key: &str| -> Vec<String> { | ||
| db.get_key_value(key) | ||
| .map(|s| { | ||
| s.trim_matches('"') | ||
| .split(',') | ||
| .map(|s| s.trim().to_string()) | ||
| .filter(|s| !s.is_empty()) | ||
| .collect() | ||
| }) | ||
| .unwrap_or_default() | ||
| }; |
There was a problem hiding this comment.
Fragile JSON string unwrapping via
trim_matches
The settings API stores all values JSON-encoded (see settings.rs line 72: serde_json::to_string(&value.0)). So a string like "moz-extension://abc" is persisted in the database as "\"moz-extension://abc\"".
trim_matches('"') strips surrounding double-quote characters, which works for simple ASCII strings, but is not a correct JSON string deserializer. It will silently produce wrong results for values that contain escaped characters. It also won't handle the case where the user stores a JSON array (e.g., ["ext1", "ext2"]) instead of a comma-separated string.
The idiomatic fix is to use serde_json::from_str::<String>(&s):
let parse_cors_list = |key: &str| -> Vec<String> {
db.get_key_value(key)
.ok()
.and_then(|s| serde_json::from_str::<String>(&s).ok())
.map(|s| {
s.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
})
.unwrap_or_default()
};
aw-server/src/endpoints/cors.rs
Outdated
| let mut allowed_regex_origins = vec!["chrome-extension://nglaklhklhcoonedhgnpgddginnjdadi".to_string()]; | ||
|
|
||
| let settings_origins = parse_cors_list("settings.cors_origins"); | ||
| drop(db); | ||
|
|
||
| let all_origins = config.cors.iter().cloned().chain(settings_origins); | ||
| for origin in all_origins { | ||
| if origin.starts_with("http://") || origin.starts_with("https://") { | ||
| allowed_exact_origins.push(origin); | ||
| } else if origin.starts_with("chrome-extension://") || origin.starts_with("moz-extension://") { | ||
| allowed_regex_origins.push(origin); | ||
| } else { | ||
| log::warn!("Ignoring invalid CORS origin: '{}'", origin); | ||
| } | ||
| } |
There was a problem hiding this comment.
Firefox extension support is effectively broken for all existing users
The original code included the comment:
Every version of a mozilla extension has its own ID to avoid fingerprinting, so we unfortunately have to allow all extensions to have access to aw-server
This is a fundamental Firefox security property: each installation (and each update) of a Firefox extension gets a new random UUID as its origin (moz-extension://<uuid>). This means:
- There is no single stable ID that a user can add to the allow-list for the ActivityWatch Firefox extension.
- After every extension update, the old ID is invalidated and the user would need to find their new UUID and update the server settings.
- Most users have no easy way to discover their current
moz-extension://UUID.
The PR removes moz-extension://.* without providing any workable alternative for Firefox users. Consider either:
- Keeping
moz-extension://.*as the default (preserving existing behavior) with clear documentation about the risk, or - Using a permanent addon ID via
browser_specific_settings.gecko.idinmanifest.jsonand documenting that users of unofficial builds need the wildcard.
aw-server/src/endpoints/mod.rs
Outdated
| config.address, config.port | ||
| ); | ||
| let cors = cors::cors(&config); | ||
| let cors = cors::cors(&config, &server_state.datastore); |
35a5fe8 to
61a534b
Compare
81acd82 to
b585230
Compare
| #[serde(default = "default_true")] | ||
| pub allow_aw_chrome_extension_from_settings: bool, | ||
|
|
||
| #[serde(default = "default_false")] |
There was a problem hiding this comment.
i'm hesitating to set default value to True, so it doesn't impact the actual users
b585230 to
c44c7df
Compare
aw-server/src/config.rs
Outdated
| cors_from_settings: default_cors(), | ||
| cors_regex_from_settings: default_cors(), | ||
| allow_aw_chrome_extension_from_settings: default_true(), | ||
| allow_all_mozilla_extension_from_settings: default_false(), |
There was a problem hiding this comment.
Not a fan of this many additional config keys. Should be prefixed consistently with cors_ such as cors_allow_aw_chrome_extension and cors_allow_all_mozilla_extension.
Curious why the _from_settings is passed along and not resolved/merged into the non-from_settings variants?
There was a problem hiding this comment.
merged with the existing vars, and explained in the readme file
aw-server/src/endpoints/mod.rs
Outdated
| if let Ok(raw) = db.get_key_value("settings.cors") { | ||
| config.cors_from_settings = parse_cors_list(raw); | ||
| } | ||
| if let Ok(raw) = db.get_key_value("settings.cors_regex") { | ||
| config.cors_regex_from_settings = parse_cors_list(raw); | ||
| } | ||
| if let Ok(raw) = db.get_key_value("settings.allow_aw_chrome_extension") { | ||
| config.allow_aw_chrome_extension_from_settings = parse_bool(raw); | ||
| } | ||
| if let Ok(raw) = db.get_key_value("settings.allow_all_mozilla_extension") { | ||
| config.allow_all_mozilla_extension_from_settings = parse_bool(raw); | ||
| } | ||
| } |
There was a problem hiding this comment.
I'm a little bit paranoid about letting web UI set CORS like that...
There was a problem hiding this comment.
do you have another suggestion to let non-devs configure cors ?
c44c7df to
4b8e893
Compare
4b8e893 to
102e1f1
Compare
2e6e544 to
e3c3227
Compare
72cdf68 to
51d5a4a
Compare
024a3bf to
8f285a5
Compare
823c6fb to
907e640
Compare
aw-server/src/endpoints/mod.rs
Outdated
| // Sync settings between Config file and Database. | ||
| // On the first run (when a key is missing in the DB), we seed the DB with the value from the config file. | ||
| // On subsequent runs, we always prefer the DB value (which might have been changed via the UI). |
There was a problem hiding this comment.
This seems problematic. It's not acceptable to have file-level config options being ignored by auto-set db-values. Makes the config file mostly useless/unreliable. Not really any "sync" happening here.
imo, order of settings/config precedence in server should be cli options -> env vars -> config file -> webui/api settings -> defaults.
One way to resolve this would be to make the webui config write directly to the config file, would effectively put them in sync (but still not a fan of exposing server config like this in api/webui tbh). But that might require a API endpoint different from /settings, maybe /config - but really unsure about this, just an idea.
Simplest would probably be to just prefer the config over the db settings and disable the webui settings if they are explicitly set in config/env/cli.
There was a problem hiding this comment.
Thank you @ErikBjare for the detailed feedback. I completely agree with the proposed precedence order (CLI > Env > Config File > WebUI/API > Defaults), and I have implemented the changes to reflect exactly this logic:
Config File Priority: the configuration file now takes absolute precedence over the database. If a CORS field is explicitly defined in config.toml, the server uses that value and ignores any existing database overrides for that specific field.
for CLI/Env: I verified that cors settings are not currently managed via CLI or Env variables in aw-server-rust. However, the current architecture is future-proof: since the database fallback only triggers for fields missing from the static configuration, any future addition of CLI/Env overrides would naturally take precedence like port and address.
UI Feedback & Safety: To address the concern about "unreliable" config, the Web UI now detects which fields are defined in the config.toml. These fields are marked in the UI as "Fixed in config file" (highlighted in orange) and the inputs are disabled. This ensures the user is aware that the file-level config is in control and cannot be bypassed via the API.
b175b15 to
32b20d7
Compare
Fixes a security issue where any Firefox extension (moz-extension://.*) could access the ActivityWatch server without any restriction. Previously, the CORS configuration included a wildcard for all Mozilla extensions by default. This commit removes that blanket permission and introduces granular control through both static configuration and the Web UI. We've added 2 new fields to the file configuration (allow_aw_chrome_extension and allow_all_mozilla_extension) and 4 new settings to the Web UI (Fixed origins, Regex origins, and extension-specific shortcuts). The server now merges these settings to determine the final set of authorized origins, ensuring a more secure and flexible configuration. Dependent on: ActivityWatch/aw-server-rust#581 edited according to the last changes
|
Tip: Greploop — Automatically fix all review issues by running Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal. |
32b20d7 to
c8695d5
Compare
Fixes a security issue where any Firefox extension (moz-extension://.*) could access the ActivityWatch server without any restriction. Previously, the CORS configuration included a wildcard for all Mozilla extensions by default. This commit removes that blanket permission and introduces granular control through both static configuration and the Web UI. We've added 2 new fields to the file configuration (allow_aw_chrome_extension and allow_all_mozilla_extension) and 4 new settings to the Web UI (Fixed origins, Regex origins, and extension-specific shortcuts). The server now merges these settings to determine the final set of authorized origins, ensuring a more secure and flexible configuration. Dependent on: ActivityWatch/aw-server-rust#581 edited according to the last changes
Fixes a security issue where any Firefox extension (moz-extension://.*) could access the ActivityWatch server without any restriction. Previously, the CORS configuration included a wildcard for all Mozilla extensions by default. This commit removes that blanket permission and introduces granular control through both static configuration and the Web UI. We've added 2 new fields to the file configuration (allow_aw_chrome_extension and allow_all_mozilla_extension) and 4 new settings to the Web UI (Fixed origins, Regex origins, and extension-specific shortcuts). The server now merges these settings to determine the final set of authorized origins, ensuring a more secure and flexible configuration. Dependent on: ActivityWatch/aw-server-rust#581 edited according to the last changes
Fixes a security issue where any Firefox extension (moz-extension://.*) could access the ActivityWatch server without any restriction. Previously, the CORS configuration included a wildcard for all Mozilla extensions by default. This commit removes that blanket permission and introduces granular control through both static configuration and the Web UI. We've added 2 new fields to the file configuration (allow_aw_chrome_extension and allow_all_mozilla_extension) and 4 new settings to the Web UI (Fixed origins, Regex origins, and extension-specific shortcuts). The server now merges these settings to determine the final set of authorized origins, ensuring a more secure and flexible configuration. Dependent on: ActivityWatch/aw-server-rust#581 edited according to the last changes
c8695d5 to
5b85c03
Compare
Fixes a security issue where any Firefox extension (moz-extension://.*) could access the ActivityWatch server without any restriction. Previously, the CORS configuration included a wildcard for all Mozilla extensions by default. This commit removes that blanket permission and introduces granular control through both static configuration and the Web UI. We've added 2 new fields to the file configuration (allow_aw_chrome_extension and allow_all_mozilla_extension) and 4 new settings to the Web UI (Fixed origins, Regex origins, and extension-specific shortcuts). The server now merges these settings to determine the final set of authorized origins, ensuring a more secure and flexible configuration. Dependent on: ActivityWatch/aw-server-rust#581 edited according to the last changes
Fixes a security issue where any Firefox extension (moz-extension://.*) could access the ActivityWatch server without any restriction. Previously, the CORS configuration included a wildcard for all Mozilla extensions by default. This commit removes that blanket permission and introduces granular control through both static configuration and the Web UI. We've added 2 new fields to the file configuration (allow_aw_chrome_extension and allow_all_mozilla_extension) and 4 new settings to the Web UI (Fixed origins, Regex origins, and extension-specific shortcuts). The server now merges these settings to determine the final set of authorized origins, ensuring a more secure and flexible configuration. Dependent on: ActivityWatch/aw-webui#795 edited according to the last changes
5b85c03 to
4b48d81
Compare
Fixes a security issue where any Firefox extension (moz-extension://.*) could access the ActivityWatch server without any restriction. Previously, the CORS configuration included a wildcard for all Mozilla extensions by default. This commit removes that blanket permission and introduces granular control through both static configuration and the Web UI. We've added 2 new fields to the file configuration (allow_aw_chrome_extension and allow_all_mozilla_extension) and 4 new settings to the Web UI (Fixed origins, Regex origins, and extension-specific shortcuts). The server now merges these settings to determine the final set of authorized origins, ensuring a more secure and flexible configuration. Dependent on: ActivityWatch/aw-server-rust#581 edited according to the last changes
[SEC] restrict CORS to authorized extension IDs
Fixes a security issue where any Firefox extension (moz-extension://.*) could access the ActivityWatch server without any restriction.
Previously, the CORS configuration included a wildcard for all Mozilla extensions by default. This commit removes that blanket permission and introduces granular control through both static configuration and the Web UI.
We've added 2 new fields to the file configuration (allow_aw_chrome_extension and allow_all_mozilla_extension) and 4 new settings to the Web UI (Fixed origins, Regex origins, and extension-specific shortcuts). The server now merges these settings to determine the final set of authorized origins, ensuring a more secure and flexible configuration.
Dependent on: ActivityWatch/aw-webui#795